import 'dart:convert';
import 'dart:math' show max;
import 'package:dbio_utils/generic_filehandle/generic_file_handle.dart';
import 'package:dbio_utils/generic_filehandle/local_file.dart';

class IndexEntry {
  int id;
  String name;
  int length;
  int start;
  int end;
  int offset;
  int lineLength;
  int lineBytes;

  IndexEntry({this.id, this.name, this.length, this.start, this.end, this.offset, this.lineLength, this.lineBytes});

  @override
  String toString() {
    return 'IndexEntry{id: $id, name: $name, length: $length, start: $start, end: $end, offset: $offset, lineLength: $lineLength, lineBytes: $lineBytes}';
  }
}

class IndexedFasta {
  GenericFileHandle fasta;
  GenericFileHandle fai;
  int chunkSizeLimit;

  Map<String, Map<String, IndexEntry>> indexList;

  IndexedFasta({
    this.fasta,
    this.fai,
    this.chunkSizeLimit = 1000000,
  });

  Future<Map<String, Map<String, IndexEntry>>> get indexes async {
    if (indexList == null) indexList = await _readFAI();
    return indexList;
  }

  Future<Map<String, Map<String, IndexEntry>>> _readFAI() async {
    Map<String, IndexEntry> indexByName = {};
    Map<String, IndexEntry> indexById = {};
    String text = await this.fai.readAsString(encoding: utf8);

    if (text == null || text.length == 0) {
      throw Exception('No data read from FASTA index (FAI) file');
    }

    var idCounter = 0;
    Map currSeq;
    text.split(RegExp('\r?\n'))
       // .where((line) => RegExp('\S').hasMatch(line))
       .forEach((line) {
      if (line == null || line.length == 0) return;
      List<String> row = line.split('\t');
      // print('row: $row');
      if (row[0] == '') return;

      if (currSeq == null || currSeq['name'] != row[0]) {
        currSeq = {'name': row[0], 'id': idCounter};
        idCounter += 1;
      }

      IndexEntry entry = IndexEntry(
        id: currSeq['id'],
        name: row[0],
        length: int.parse(row[1]),
        start: 0,
        end: int.parse(row[1]),
        offset: int.parse(row[2]),
        lineLength: int.parse(row[3]),
        lineBytes: int.parse(row[4]),
      );

      indexByName[entry.name] = entry;
      indexById['${entry.id}'] = entry;
    });

    return {'name': indexByName, 'id': indexById};
  }

  Future<List> get sequenceList async {
    return (await indexes)['id'].values.map((e) => e.name).toList();
  }

  /// @returns {array[string]} array of string sequence
  /// names that are present in the index, in which the
  /// array index indicates the sequence ID, and the value
  /// is the sequence name
  Future<Map<String, IndexEntry>> getSequenceNames() async {
    return (await indexes)['name'];
  }

  /// @returns {array[string]} array of string sequence
  /// names that are present in the index, in which the
  /// array index indicates the sequence ID, and the value
  /// is the sequence name
  Future<Map> getSequenceSizes() async {
    Map returnObject = {};
    List<IndexEntry> vals = (await indexes)['id'].values.toList();
    for (var i = 0; i < vals.length; i += 1) {
      returnObject[vals[i].name] = vals[i].length;
    }
    return returnObject;
  }

  /// @returns {array[string]} array of string sequence
  /// names that are present in the index, in which the
  /// array index indicates the sequence ID, and the value
  /// is the sequence name
  Future<int> getSequenceSize(seqName) async {
    var idx = (await indexes)['name'];
    return idx?.length;
  }

  ///
  /// @param {string} name
  /// @returns {Promise[boolean]} true if the file contains the given reference sequence name
  Future<bool> hasReferenceSequence(name) async {
    return (await indexes)['name'].containsKey(name);
  }

  ///
  /// @param {number} seqId
  /// @param {number} min
  /// @param {number} max
  Future<String> getResiduesById(seqId, int min, int max) async {
    var indexEntry = (await this.indexes)['id'][seqId];
    if (indexEntry == null) return null;
    return this._fetchFromIndexEntry(indexEntry, min, max);
  }

  Future<String> getResiduesByName(String seqName, int min, int max) async {
    var indexEntry = (await this.indexes)['id'][seqName];
    if (indexEntry == null) return null;
    return this._fetchFromIndexEntry(indexEntry, min, max);
  }

  Future<String> _fetchFromIndexEntry(IndexEntry indexEntry, int min, int max) async {
    var end = max;
    if (min < 0) {
      throw new Exception('regionStart cannot be less than 0');
    }
    if (end == null || end > indexEntry.length) {
      end = indexEntry.length;
    }
    if (min >= end) {
      return '';
    }

    int position = _faiOffset(indexEntry, min);
    int readLen = _faiOffset(indexEntry, end) - position;

    if (readLen > this.chunkSizeLimit) {
      throw Exception(
        'data size of $readLen bytes exceeded chunk size limit of ${this.chunkSizeLimit} bytes',
      );
    }
    print('read readLen:$readLen, position:$position');

    List<int> buffer = List.filled(readLen, -1);
    var bytes = await this.fasta.read(0, readLen, position);
    String content = utf8.decode(bytes).replaceAll('/\s+/', '');

    return content;
  }

  int _faiOffset(IndexEntry idx, int position) {
    return (idx.offset + idx.lineBytes * (position / idx.lineLength).floor() + (position % idx.lineLength));
  }
}
